/*
 * MIT License
 *
 * Copyright (c) 2020 wen.gu <454727014@qq.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

/***************************************************************************
* Name: directx_video_sink.cpp
*
* Purpose: video sink component implementation with directx video.
*
* Developer:
*   wen.gu , 2018-09-07
*
* TODO:
*
***************************************************************************/

/******************************************************************************
**    INCLUDES
******************************************************************************/
#define _CRT_SECURE_NO_WARNINGS

#include <string.h>
#include <windows.h>
#include <mmsystem.h>
#include <process.h>
#include <list>
#include <tchar.h>

#include "directx_video_sink.h"
#include "mp_utils.h"

extern "C"
{
#include "libswscale/swscale.h" 
}


#define LOG_TAG "dx_video"
#include "mp_log.h"


#pragma comment(lib, "winmm.lib")

/******************************************************************************
**    MACROS
******************************************************************************/

#define DX_COMP_NAME "directx video"
#define DX_COMP_VER "00.01.00"


/******************************************************************************
**    VARIABLE DEFINITIONS
******************************************************************************/

class MyGDIRender
{
public:
    MyGDIRender();
    ~MyGDIRender();

public:
    MPError open(const MPTrackInfo& info);
    MPError close();

    MPError render(MPBuffer* frame);
    MPError setDisplayRegion(uint32_t x, uint32_t y, uint32_t width, uint32_t height);
    MPError setVisible(bool visible);

protected:
    MPError windowCreate();
    MPError windowDestroy();

protected:
    void onWindowEvent();
private:
    bool mIsRequestExit = false;
    HDC mHdc = nullptr;
    HWND mHwnd = nullptr; /** the handle of window */
    HINSTANCE mHInstance = nullptr;
                         /**name for window */
    LPCSTR mLpClassName = nullptr;
    LPCSTR mLpWindowName = nullptr;
    int mScreenWidth = 500;
    int mScreenHeight = 500;
    int mVideoWidth = 0;
    int mVideoHeight = 0;
    unsigned char* mRenderBuf = nullptr;
    SwsContext* swsCtx = nullptr;

    std::thread* mThd = nullptr;
};


/******************************************************************************
**   inner FUNCTION DEFINITIONS
******************************************************************************/
//Not Efficient, Just an example
//change endian of a pixel (32bit)
static void CHANGE_ENDIAN_32(unsigned char *data) {
    char temp3, temp2;
    temp3 = data[3];
    temp2 = data[2];
    data[3] = data[0];
    data[2] = data[1];
    data[0] = temp3;
    data[1] = temp2;
}
//change endian of a pixel (24bit)
static void CHANGE_ENDIAN_24(unsigned char *data) {
    char temp2 = data[2];
    data[2] = data[0];
    data[0] = temp2;
}

//RGBA to RGB24 (or BGRA to BGR24)
static void CONVERT_RGBA32toRGB24(unsigned char *image, int w, int h) {
    for (int i = 0; i<h; i++)
        for (int j = 0; j<w; j++) {
            memcpy(image + (i*w + j) * 3, image + (i*w + j) * 4, 3);
        }
}
//RGB24 to BGR24
static void CONVERT_RGB24toBGR24(unsigned char *image, int w, int h) {
    for (int i = 0; i<h; i++)
        for (int j = 0; j<w; j++) {
            char temp2;
            temp2 = image[(i*w + j) * 3 + 2];
            image[(i*w + j) * 3 + 2] = image[(i*w + j) * 3 + 0];
            image[(i*w + j) * 3 + 0] = temp2;
        }
}

//Change endian of a picture
static void CHANGE_ENDIAN_PIC(unsigned char *image, int w, int h, int bpp) {
    unsigned char *pixeldata = NULL;
    for (int i = 0; i<h; i++)
        for (int j = 0; j<w; j++) {
            pixeldata = image + (i*w + j)*bpp / 8;
            if (bpp == 32) {
                CHANGE_ENDIAN_32(pixeldata);
            }
            else if (bpp == 24) {
                CHANGE_ENDIAN_24(pixeldata);
            }
        }
}

inline unsigned char CONVERT_ADJUST(double tmp)
{
    return (unsigned char)((tmp >= 0 && tmp <= 255) ? tmp : (tmp < 0 ? 0 : 255));
}
//YUV420P to RGB24
static void CONVERT_YUV420PtoRGB24(unsigned char* yuv_src, unsigned char* rgb_dst, int nWidth, int nHeight)
{
    unsigned char *tmpbuf = (unsigned char *)malloc(nWidth*nHeight * 3);
    unsigned char Y, U, V, R, G, B;
    unsigned char* y_planar, *u_planar, *v_planar;
    int rgb_width, u_width;
    rgb_width = nWidth * 3;
    u_width = (nWidth >> 1);
    int ypSize = nWidth * nHeight;
    int upSize = (ypSize >> 2);
    int offSet = 0;

    y_planar = yuv_src;
    u_planar = yuv_src +ypSize;
    v_planar = u_planar +upSize;

    for (int i = 0; i < nHeight; i++)
    {
        for (int j = 0; j < nWidth; j++)
        {
            // Get the Y value from the y planar
            Y = *(y_planar + nWidth * i + j);
            // Get the V value from the u planar
            offSet = (i >> 1) * (u_width)+(j >> 1);
            V = *(u_planar + offSet);
            // Get the U value from the v planar
            U = *(v_planar + offSet);

            // Cacular the R,G,B values
            // Method 1
            R = CONVERT_ADJUST((Y + (1.4075 * (V - 128))));
            G = CONVERT_ADJUST((Y - (0.3455 * (U - 128) - 0.7169 * (V - 128))));
            B = CONVERT_ADJUST((Y + (1.7790 * (U - 128))));
            /*
            // The following formulas are from MicroSoft' MSDN
            int C,D,E;
            // Method 2
            C = Y - 16;
            D = U - 128;
            E = V - 128;
            R = CONVERT_ADJUST(( 298 * C + 409 * E + 128) >> 8);
            G = CONVERT_ADJUST(( 298 * C - 100 * D - 208 * E + 128) >> 8);
            B = CONVERT_ADJUST(( 298 * C + 516 * D + 128) >> 8);
            R = ((R - 128) * .6 + 128 )>255?255:(R - 128) * .6 + 128;
            G = ((G - 128) * .6 + 128 )>255?255:(G - 128) * .6 + 128;
            B = ((B - 128) * .6 + 128 )>255?255:(B - 128) * .6 + 128;
            */

            offSet = rgb_width * i + j * 3;

            rgb_dst[offSet] = B;
            rgb_dst[offSet + 1] = G;
            rgb_dst[offSet + 2] = R;
        }
    }
    free(tmpbuf);
}


static LRESULT WINAPI MyWndProc(HWND hwnd, UINT msg, WPARAM wparma, LPARAM lparam)
{
    switch (msg) {
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
        break;
    case WM_SIZE:
        break;
    default:
        break;
    }

    return DefWindowProc(hwnd, msg, wparma, lparam);
}


MyGDIRender::MyGDIRender()
{
    /** todo something */

}

MyGDIRender::~MyGDIRender()
{
    /** todo something */
}

///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
MPError MyGDIRender::open(const MPTrackInfo& info)
{
    MPError ret = windowCreate();

    if (MP_OK == ret)
    {
        mScreenWidth = info.video.picWidth;
        mScreenHeight = info.video.picHeight;
    }

    return ret;
}

MPError MyGDIRender::close()
{
    return windowDestroy();
}


MPError MyGDIRender::render(MPBuffer* frame)
{
    HDC hdc = GetDC(mHwnd);
    RECT rect;
    GetWindowRect(mHwnd, &rect);
    int screen_w = rect.right - rect.left;
    int screen_h = rect.bottom - rect.top;

    if ((screen_w != mScreenWidth) || (screen_h != mScreenHeight))
    {
        mScreenWidth = screen_w;
        mScreenHeight = screen_h;
    }

    BufferInfo& bi = frame->info();
    int pixel_w = bi.video.picWidth;
    int pixel_h = bi.video.picHeight;



    if ((pixel_w != mVideoWidth) || (pixel_h != mVideoHeight))
    {
        if (mRenderBuf)
        {
            delete mRenderBuf;
        }
        mVideoWidth = pixel_w;
        mVideoHeight = pixel_h;
        mRenderBuf = new unsigned char[mVideoWidth * mVideoHeight * 3];
    }



    //YUV Need to Convert to RGB first
    //YUV420P to RGB24

#if 0
    CONVERT_YUV420PtoRGB24(frame->data(), mRenderBuf, pixel_w, pixel_h);
    //Change to Little Endian
    CHANGE_ENDIAN_PIC(mRenderBuf, pixel_w, pixel_h, 24);
#else


    if (swsCtx == nullptr)
    {
        swsCtx = sws_getContext(pixel_w, pixel_h, AV_PIX_FMT_YUV420P, pixel_w, pixel_h, AV_PIX_FMT_BGR24, SWS_BICUBIC, NULL, NULL, NULL);
    }

    int ypSize = pixel_w * pixel_h;
    int upSize = (ypSize >> 2);


    unsigned char* yuv_src = frame->data();
    unsigned char* myBuf[4] = { 0 };
    unsigned char* outBuf[4] = { 0 };

    myBuf[0] = yuv_src;
    myBuf[1] = yuv_src + ypSize;
    myBuf[2] = myBuf[1] + upSize;

    outBuf[0] = mRenderBuf;
    outBuf[1] = mRenderBuf;
    int in_line_size[4] = { pixel_w, pixel_w/2, pixel_w/2 };
    int out_line_size[4] = { 0 };
    out_line_size[0] = pixel_w * 3;
    out_line_size[1] = pixel_w;
    out_line_size[2] = pixel_w;




    int ret = sws_scale(swsCtx, myBuf, (const int*)in_line_size, 0, pixel_h, outBuf, out_line_size);
#endif
    //BMP Header
    BITMAPINFO m_bmphdr = { 0 };
    DWORD dwBmpHdr = sizeof(BITMAPINFO);
    //24bit
    m_bmphdr.bmiHeader.biBitCount = 24;
    m_bmphdr.bmiHeader.biClrImportant = 0;
    m_bmphdr.bmiHeader.biSize = dwBmpHdr;
    m_bmphdr.bmiHeader.biSizeImage = 0;
    m_bmphdr.bmiHeader.biWidth = pixel_w;
    //Notice: BMP storage pixel data in opposite direction of Y-axis (from bottom to top).
    //So we must set reverse biHeight to show image correctly.
    m_bmphdr.bmiHeader.biHeight = -pixel_h;
    m_bmphdr.bmiHeader.biXPelsPerMeter = 0;
    m_bmphdr.bmiHeader.biYPelsPerMeter = 0;
    m_bmphdr.bmiHeader.biClrUsed = 0;
    m_bmphdr.bmiHeader.biPlanes = 1;
    m_bmphdr.bmiHeader.biCompression = BI_RGB;

    //YUV420P data convert to another buffer
    int nResult = StretchDIBits(hdc, 0, 0, mScreenWidth, mScreenHeight,
                                0, 0, pixel_w, pixel_h, mRenderBuf,
                                &m_bmphdr, DIB_RGB_COLORS, SRCCOPY);

    ReleaseDC(mHwnd, hdc);

    return MP_OK;
}

MPError MyGDIRender::setDisplayRegion(uint32_t x, uint32_t y, uint32_t width, uint32_t height)
{
    if (MoveWindow(mHwnd, x, y, width, height, TRUE) == TRUE)
    {
        return MP_OK;
    }

    return MP_ErrorUndefined;
}

MPError MyGDIRender::setVisible(bool visible)
{
    if (ShowWindow(mHwnd, visible ? SW_SHOW : SW_HIDE) == TRUE)
    {
        return MP_OK;
    }

    return MP_ErrorUndefined;
}

///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////

MPError MyGDIRender::windowCreate()
{
    MPError ret = MP_ErrorInsufficientResources;

    mThd = new std::thread(std::bind(&MyGDIRender::onWindowEvent, this));

    if (mThd)
    {
        ret = MP_OK;
    }        
   

    return ret;
}


MPError MyGDIRender::windowDestroy()
{
    mIsRequestExit = true;

    if (mThd)
    {
        mThd->join();
        delete mThd;
    }    

    UnregisterClass(mLpClassName, mHInstance);

    if (mRenderBuf)
    {
        delete mRenderBuf;
        mRenderBuf = nullptr;
    }

    return MP_OK;
}


void MyGDIRender::onWindowEvent()
{   
    WNDCLASSEX wc;
    ZeroMemory(&wc, sizeof(wc));

    wc.cbSize = sizeof(wc);
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wc.lpfnWndProc = (WNDPROC)MyWndProc;
    wc.lpszClassName = _T("GDI");
    wc.style = CS_HREDRAW | CS_VREDRAW;

    RegisterClassEx(&wc);
    mHInstance = GetModuleHandle(nullptr);

    HWND hwnd = nullptr;
    hwnd = CreateWindow(_T("GDI"), _T("GDI video render"),  WS_OVERLAPPEDWINDOW,
                        100, 100, 500, 500, nullptr, nullptr, mHInstance, nullptr);

    if (hwnd)
    {        
        MSG msg;
        mHwnd = hwnd;
        ZeroMemory(&msg, sizeof(msg));
        ShowWindow(hwnd, SW_SHOW);
        UpdateWindow(hwnd);

        while (msg.message != WM_QUIT)
        {
            //PeekMessage, not GetMessage
            if (PeekMessage(&msg, mHwnd, 0, 0, PM_REMOVE))
            {
                TranslateMessage(&msg);
                DispatchMessage(&msg);
            }
            else 
            {
                Sleep(10);
            }

            if (mIsRequestExit)
            {
                PostMessage(mHwnd, WM_DESTROY, 0, 0);
                break;
            }
        }
    }

   
    printf("run out\n");
}

/******************************************************************************
**   FUNCTION DEFINITIONS
******************************************************************************/

DxVideoSink::DxVideoSink()
    :MPVideoSink(DX_COMP_NAME, DX_COMP_VER)
{
    /** todo something */
    mRender = new MyGDIRender;
}

DxVideoSink::~DxVideoSink()
{
    /** todo something */
    if (mRender)
    {
        mRender->close();
        delete mRender;
        mRender;
    }
}

///////////////////////////////////////////////////////////////////////////////

MPError DxVideoSink::display(MPBuffer* frame)
{
    MPError ret = MP_ErrorInvalidStatus;

    if (mRender)
    {
        if (frame)
        {
            ret = mRender->render(frame);
        }
        else
        {
            ret = MP_ErrorBadParameter;
        }        
    }

    return ret;
}

MPError DxVideoSink::setDisplayRegion(uint32_t x, uint32_t y, uint32_t width, uint32_t height)
{
    MPError ret = MP_ErrorInvalidStatus;

    if (mRender)
    {
        ret = mRender->setDisplayRegion(x, y, width, height);
    }

    return ret;
}

MPError DxVideoSink::setVisible(bool visible)
{
    MPError ret = MP_ErrorInvalidStatus;

    if (mRender)
    {
        ret = mRender->setVisible(visible);
    }

    return ret;
}

MPError DxVideoSink::initialize(const MPTrackInfo& info)
{
    MPError ret = MP_ErrorInvalidStatus;

    if (mRender)
    {
        ret = mRender->open(info);
    }

    return ret;
}

